package de.mmis.core.tuplespace;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import org.apache.log4j.Logger;

import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.tuplespace.Notification.Command;

/**
 * map implementation using tuplespace, where all operations are synchronized.
 * is it a bit slower than the ansyc implementation, but don't expect the user
 * to know which elements are present and which are not. Should be used whenever
 * it is not sure that a map element really exists. Do not use on async map!
 * 
 * @author Christian Koch
 * 
 */
public class TuplespaceSynchronizedMap extends TuplespaceContainer implements
		Map<Tree, Tree>, Observable<TuplespaceEvent> {

	private final static Logger LOGGER = Logger
			.getLogger(TuplespaceSynchronizedMap.class);

	private static final String MAP_ELEM_NAME_SUFFIX = "-map-elem";
	private static final String MAP_NAME_SUFFIX = "-map";
	private static final String MAP_NAME_PREFIX = ":";

	private final Tuplespace space;
	private final String name;
	private final LeafNode mapName;
	private final LeafNode mapElemName;
	private final Tree id;
	private final InnerNode mapTuple;
	private final InnerNode mapWriteMutexTuple;
	private final InnerNode mapElemTuple;

	private final ArrayList<Observer<TuplespaceEvent>> observers;

	/**
	 * creates new tuplespace map
	 * 
	 * @param space
	 *            tuplespace client connected to used server
	 * @param name
	 *            map name (combined name and id should be unique)
	 * @param id
	 *            object id map belongs to (combined name and id should be
	 *            unique)
	 * @throws TuplespaceException
	 */
	public TuplespaceSynchronizedMap(Tuplespace space, String name, Tree id)
			throws TuplespaceException {
		this(space, name, id, DEFAULT_TIMEOUT, DEFAULT_LEASETIME);
	}

	/**
	 * creates new tuplespace map, that destroy itself when not used for a
	 * specific time
	 * 
	 * @param space
	 *            tuplespace client connected to used server
	 * @param name
	 *            map name (combined name and id should be unique)
	 * @param id
	 *            object id map belongs to (combined name and id should be
	 *            unique)
	 * @param timeout
	 *            time to live between two consecutive insert or delete
	 *            operations
	 * @throws TuplespaceException
	 */
	public TuplespaceSynchronizedMap(Tuplespace space, String name, Tree id,
			long timeout) throws TuplespaceException {
		this(space, name, id, timeout, DEFAULT_LEASETIME);
	}

	/**
	 * creates new tuplespace map, that destroy itself when not used for a
	 * specific time
	 * 
	 * @param space
	 *            tuplespace client connected to used server
	 * @param name
	 *            map name (combined name and id should be unique)
	 * @param id
	 *            object id map belongs to (combined name and id should be
	 *            unique)
	 * @param timeout
	 *            default time span to wait trying to take map tuple
	 * @param leaseTime
	 *            time to live between two consecutive insert or delete
	 *            operations
	 * @throws TuplespaceException
	 */
	public TuplespaceSynchronizedMap(Tuplespace space, String name, Tree id,
			long timeout, long leaseTime) throws TuplespaceException {
		super(timeout, leaseTime);
		this.space = space;
		this.name = name;
		this.id = id;
		this.mapName = new LeafNode(
				(MAP_NAME_PREFIX + this.name + MAP_NAME_SUFFIX));
		this.mapElemName = new LeafNode(
				(MAP_NAME_PREFIX + this.name + MAP_ELEM_NAME_SUFFIX));

		this.mapTuple = new InnerNode(this.mapName, this.id);
		this.mapWriteMutexTuple = new InnerNode(this.mapName, this.id,
				new LeafNode(":write-mutex"));
		this.mapElemTuple = new InnerNode(this.mapElemName, this.id,
				new LeafNode("?"), new LeafNode("?"));

		observers = new ArrayList<Observer<TuplespaceEvent>>();

		Notification notification = new Notification(Command.WRITE,
				mapWriteMutexTuple);
		space.notify(notification);

		space.addObserver(new Observer<TuplespaceEvent>() {
			@Override
			public void notify(Observable<? extends TuplespaceEvent> sender,
					TuplespaceEvent event) {
				if (mapWriteMutexTuple.equals(event.getTemplate()))
					fireEvent(event);
			}
		});
	}

	/**
	 * initializes the map tuple (not needed if already exists, needs to be done
	 * before any map operations can be executed)
	 * 
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public void initMap() throws TuplespaceException {
		deinitMap(0);

		space.write(mapTuple, leaseTime);
		space.write(mapWriteMutexTuple, leaseTime);
	}

	/**
	 * checks if map tuple is present on server, using default timeout
	 * 
	 * @return true, if map tuple is present, else false
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public boolean isPresent() throws TuplespaceException {
		return isPresent(getTimeout());
	}

	/**
	 * checks if map tuple is present on server
	 * 
	 * @param timeout
	 *            time span to wait for map tuple to appear. If set to 0, a
	 *            single readIfExists check will be made.
	 * @return true, if map tuple is present, else false
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public boolean isPresent(long timeout) throws TuplespaceException {
		if (timeout <= 0)
			return space.readIfExists(mapTuple) != null;
		if (space.read(mapTuple, timeout) == null)
			return false;
		return true;
	}

	/**
	 * deletes the map tuple and all elements, using default timeout
	 * 
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public void deinitMap() throws TuplespaceException {
		deinitMap(getTimeout());
	}

	/**
	 * deletes the map tuple and all elements
	 * 
	 * @param timeout
	 *            time span to wait trying to remove map tuple. If set to 0, a
	 *            single takeIfExists will be made
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 */
	public void deinitMap(long timeout) throws TuplespaceException {
		if (timeout <= 0)
			space.takeIfExists(mapTuple);
		else
			space.take(mapTuple, timeout);

		if (timeout <= 0)
			space.takeIfExists(mapWriteMutexTuple);
		else
			space.take(mapWriteMutexTuple, timeout);

		while (space.takeIfExists(mapElemTuple) != null) {
			// take until empty
		}
	}

	/**
	 * create or change value for specified key, using default timeout and
	 * leaseTime
	 * 
	 * @param key
	 *            key to set value for
	 * @param value
	 *            value to set
	 * @return previous value of this element, or null, if it did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree setValue(Tree key, Tree value) throws TuplespaceException,
			TimeoutException {
		return setValue(key, value, getTimeout(), leaseTime);
	}

	/**
	 * create or change value for specified key, using default leaseTime
	 * 
	 * @param key
	 *            key to set value for
	 * @param value
	 *            value to set
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return previous value of this element, or null, if it did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree setValue(Tree key, Tree value, long timeout)
			throws TuplespaceException, TimeoutException {
		return setValue(key, value, timeout, leaseTime);
	}

	/**
	 * create or change value for specified key
	 * 
	 * @param key
	 *            key to set value for
	 * @param value
	 *            value to set
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @param leaseTime
	 *            time to live for set element
	 * @return previous value of this element, or null, if it did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree setValue(Tree key, Tree value, long timeout, long leaseTime)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode mapWriteTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteTuple = this.mapWriteMutexTuple;
		}

		InnerNode temp = new InnerNode(mapElemName, id, key, new LeafNode("?"));
		Tree ret = space.takeIfExists(temp);
		if (ret != null)
			ret = ((InnerNode) ret).getSubTree(3);

		temp.setSubTree(3, value);
		space.write(temp, leaseTime);

		space.write(mapWriteTuple, this.leaseTime);
		space.write(mapTuple, this.leaseTime);

		return ret;
	}

	/**
	 * get value associated with given key, using default timeout
	 * 
	 * @param key
	 *            key to find value for
	 * @return value of given key, or null, if element does not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree getValue(Tree key) throws TuplespaceException, TimeoutException {
		return getValue(key, getTimeout());
	}

	/**
	 * get value associated with given key
	 * 
	 * @param key
	 *            key to find value for
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return value of given key, or null, if element does not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree getValue(Tree key, long timeout) throws TuplespaceException,
			TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode temp = new InnerNode(mapElemName, id, key, new LeafNode("?"));
		Tree ret = space.readIfExists(temp);

		space.write(mapTuple, leaseTime);

		if (ret == null)
			return ret;
		return ((InnerNode) ret).getSubTree(3);
	}

	/**
	 * get all keys associated with given value, using default timeout
	 * 
	 * @param value
	 *            value to find keys for
	 * @return list of keys for given value
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public List<Tree> getKeys(Tree value) throws TuplespaceException,
			TimeoutException {
		return getKeys(value, getTimeout());
	}

	/**
	 * get all keys associated with given value
	 * 
	 * @param value
	 *            value to find keys for
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return list of keys for given value
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public List<Tree> getKeys(Tree value, long timeout)
			throws TuplespaceException, TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);
		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode temp = new InnerNode(mapElemName, id, new LeafNode("?"),
				value);
		List<Tree> ret = space.scan(temp);

		space.write(mapTuple, leaseTime);

		return ret;
	}

	/**
	 * remove element with given key, using default timeout
	 * 
	 * @param key
	 *            key to delete
	 * @return value of given key, or null, if element did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree removeKey(Tree key) throws TuplespaceException,
			TimeoutException {
		return removeKey(key, getTimeout());
	}

	/**
	 * remove element with given key
	 * 
	 * @param key
	 *            key to delete
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @return value of given key, or null, if element did not exist
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public Tree removeKey(Tree key, long timeout) throws TuplespaceException,
			TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);

		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		InnerNode mapWriteTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteTuple = this.mapWriteMutexTuple;
		}

		InnerNode temp = new InnerNode(mapElemName, id, key, new LeafNode("?"));
		Tree ret = space.takeIfExists(temp);
		if (ret != null)
			ret = ((InnerNode) ret).getSubTree(3);

		space.write(mapWriteTuple, leaseTime);
		space.write(mapTuple, leaseTime);

		return ret;
	}

	/**
	 * removes all elements in map, using default timeout
	 * 
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public void removeAll() throws TuplespaceException, TimeoutException {
		removeAll(getTimeout());
	}

	/**
	 * removes all elements in map
	 * 
	 * @param timeout
	 *            time span to wait trying to remove map tuple
	 * @throws TuplespaceException
	 *             error while reading or writing to tuplespace server
	 * @throws TimeoutException
	 *             timed out while trying to take map tuple
	 */
	public void removeAll(long timeout) throws TuplespaceException,
			TimeoutException {
		InnerNode mapTuple = (InnerNode) space.take(this.mapTuple, timeout);

		InnerNode mapWriteTuple = (InnerNode) space
				.takeIfExists(this.mapWriteMutexTuple);
		if (mapWriteTuple == null) {
			LOGGER.warn("map write mutex tuple missing on locked map "
					+ mapTuple + "; generating new one");
			mapWriteTuple = this.mapWriteMutexTuple;
		}

		if (mapTuple == null)
			throw new TimeoutException("map tuple take timed out");

		while (space.takeIfExists(mapElemTuple) != null) {
			// take until empty
		}

		space.write(mapWriteTuple, leaseTime);
		space.write(mapTuple, leaseTime);
	}

	@Override
	public void clear() {
		try {
			removeAll();
		} catch (TuplespaceException e) {
			throw new IllegalStateException(e);
		} catch (TimeoutException e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public boolean containsKey(Object key) {
		try {
			if (!(key instanceof Tree))
				return false;
			if (getValue((Tree) key) == null)
				return false;

			return true;
		} catch (TuplespaceException e) {
			throw new IllegalStateException(e);
		} catch (TimeoutException e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public boolean containsValue(Object value) {
		try {
			if (!(value instanceof Tree))
				return false;
			if (getKeys((Tree) value).size() == 0)
				return false;

			return true;
		} catch (TuplespaceException e) {
			throw new IllegalStateException(e);
		} catch (TimeoutException e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public Set<java.util.Map.Entry<Tree, Tree>> entrySet() {
		throw new UnsupportedOperationException("not allowed in TuplespaceMap");
	}

	@Override
	public Tree get(Object key) {
		if (!(key instanceof Tree))
			return null;
		try {
			return getValue((Tree) key);
		} catch (TuplespaceException e) {
			throw new IllegalStateException(e);
		} catch (TimeoutException e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public boolean isEmpty() {
		return (get(new LeafNode("?")) != null);
	}

	@Override
	public Set<Tree> keySet() {
		throw new UnsupportedOperationException("not allowed in TuplespaceMap");
	}

	@Override
	public Tree put(Tree key, Tree value) {
		try {
			return setValue(key, value);
		} catch (TuplespaceException e) {
			throw new IllegalStateException(e);
		} catch (TimeoutException e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public void putAll(Map<? extends Tree, ? extends Tree> m) {
		throw new UnsupportedOperationException("not allowed in TuplespaceMap");
	}

	@Override
	public Tree remove(Object key) {
		if (!(key instanceof Tree))
			return null;
		try {
			return removeKey((Tree) key);
		} catch (TuplespaceException e) {
			throw new IllegalStateException(e);
		} catch (TimeoutException e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public int size() {
		throw new UnsupportedOperationException("not allowed in TuplespaceMap");
	}

	@Override
	public Collection<Tree> values() {
		throw new UnsupportedOperationException("not allowed in TuplespaceMap");
	}

	protected void fireEvent(TuplespaceEvent event) {
		synchronized (observers) {
			for (Observer<TuplespaceEvent> observer : observers) {
				observer.notify(this, event);
			}
		}
	}

	@Override
	public void addObserver(Observer<TuplespaceEvent> observer) {
		synchronized (observers) {
			observers.add(observer);
		}
	}

	@Override
	public void removeObserver(Observer<TuplespaceEvent> observer) {
		synchronized (observers) {
			observers.remove(observer);
		}
	}
}
